import dayjs, { Dayjs } from 'dayjs'
import 'dayjs/locale/zh-cn'
import DayOfYear from 'dayjs/plugin/dayOfYear'
import IsoWeek from 'dayjs/plugin/isoWeek'
import LocaleData from 'dayjs/plugin/localeData'
import Weekday from 'dayjs/plugin/weekday'
import WeekOfYear from 'dayjs/plugin/weekOfYear'
import WeekYear from 'dayjs/plugin/weekYear'
import { DateInfo } from '../model/DateInfo'
import { WeekInfo } from '../model/WeekInfo'
import { WeekLocation } from '../model/WeekLocation'

// 全局使用简体中文
dayjs.locale('zh-cn')

dayjs.extend(WeekOfYear)
dayjs.extend(WeekYear)
dayjs.extend(DayOfYear)
dayjs.extend(Weekday)
dayjs.extend(LocaleData)
dayjs.extend(IsoWeek)

// noinspection JSUnusedGlobalSymbols
/**
 * TimeTool
 * @author 冰凝
 * @date 2022-09-19 18:11:32
 **/
export class TimeTool {
    static readonly DATE_PATTERN = 'YYYY-MM-DD'
    static readonly TIME_PATTERN = 'HH:mm:ss'
    static readonly DATE_TIME_PATTERN = 'YYYY-MM-DD HH:mm:ss'

    /**
     * 今天时间范围 格式化为字符串
     */
    public static todayRangeStr(): [ string, string ] {
        return this.range(1, 'd')
            .map(i => i.format(this.DATE_TIME_PATTERN)) as [ string, string ]
    }

    /**
     * 今天
     */
    public static todayDateRange(): [ Date, Date ] {
        return this.dateRange(24, 'h')
    }

    /**
     * 昨天
     */
    public static yesterdayDateRange(): [ Date, Date ] {
        return this.dateRange(-24, 'h')
    }

    /**
     * 获取相对于当前日期的时间范围
     * @param variation 相对于当前日期(不含时分秒) 的变化量, 允许负值
     * @param unit 变化单位
     */
    public static dateRange(variation: number, unit: dayjs.ManipulateType = 'h'): [ Date, Date ] {
        return this.range(variation, unit).map(i => i.toDate()) as [ Date, Date ]
    }

    /**
     * 相对于今天 0 点的时间区间
     */
    public static range(variation: number, unit: dayjs.ManipulateType = 'h'): [ Dayjs, Dayjs ] {
        if (variation > 0) {
            const start = this.nowDate()
            const end = start.add(variation, unit).subtract(1, 's')
            return [ start, end ]
        } else {
            const end = this.nowDate()
            const start = end.subtract(-variation, unit)
            return [ start, end.subtract(1, 's') ]
        }
    }

    /**
     * 今天的开始时间
     */
    public static todayStart(): string {
        return this.nowDate().format(this.DATE_TIME_PATTERN)
    }

    /**
     * 今天的结束时间
     */
    public static todayEnd(): string {
        return this.nowDate().endOf('day').format(this.DATE_TIME_PATTERN)
    }

    /**
     * 时分秒清空的今天
     */
    public static nowDate(): Dayjs {
        return dayjs().startOf('day')
    }

    /**
     * 当前周信息
     */
    public static currentWeek(): WeekInfo {
        const now = this.nowDate()
        const dayOfWeek = now.day()

        if (dayOfWeek === 0) {
            const start = now.subtract(6, 'day')
            return new WeekInfo(start, now)
        } else {
            const start = now.subtract(dayOfWeek - 1, 'day')
            const end = now.add(7 - dayOfWeek, 'day')
            return new WeekInfo(start, end)
        }
    }

    /**
     * 当前周 范围
     */
    public static currentWeekRange(): Array<DateInfo> {
        const { start, end } = this.currentWeek()
        return this.daysInfo(start.display, end.display)
    }

    public static daysInfo(start: string, end: string) {
        return this.days(start, end)
            .map(day => new DateInfo(day))
    }

    /**
     * 时分秒 隐藏 秒
     * @param timeStr 时间字符串 {@link TIME_PATTERN}
     */
    public static displayTime(timeStr: string): string {
        return timeStr.substring(0, 5)
    }

    /**
     * 获得今年的开始日期
     */
    public static nowYearStartDate(): Dayjs {
        return dayjs().startOf('year')
    }

    /**
     * 解析年月日
     */
    public static parseDate(str: string): Dayjs {
        return dayjs(str, this.DATE_PATTERN)
    }

    /**
     * 解析 年月日 时分秒
     */
    public static parseDateTime(str: string): Dayjs {
        return dayjs(str, this.DATE_TIME_PATTERN)
    }

    /**
     * 计算开始日期和结束日期中所有天 [startDate, endDate]
     */
    public static days(startDate: string, endDate: string): Array<Dayjs> {
        const start = TimeTool.parseDate(startDate)
        const end = TimeTool.parseDate(endDate)

        if (end.unix() < start.unix()) {
            throw new Error(`错误的时间区间 [${ startDate } - ${ endDate }]`)
        }

        const res: Array<Dayjs> = []
        let index = 0
        do {
            const temp = start.add(index, 'day')
            res.push(temp)

            if (temp.unix() >= end.unix()) {
                break
            }
            index++
        } while (true)

        return res
    }

    /**
     * 获取指定时间的周定位信息
     * @param date 日期对象 或者 ISO 格式的日期字符串
     * @see dayjs
     * @see DATE_PATTERN
     * @see DATE_TIME_PATTERN
     */
    public static weekLocation(date: Dayjs | string): WeekLocation {
        const d = typeof date === 'string'
            ? dayjs(date)
            : date
        return new WeekLocation(d.isoWeekYear(), d.isoWeek())
    }

    /**
     * 计算从开始时间到结束时间经历的所有周信息
     * @param startDate 开始时间 年月日
     * @param endDate 结束时间 年月日
     * @return 时间段内所有周信息, {@link WeekInfo}
     */
    public static weeks(startDate: string, endDate: string): Array<WeekInfo> {
        const start = TimeTool.parseDate(startDate)
        const end = TimeTool.parseDate(endDate)
        if (end.unix() < start.unix()) {
            throw new Error(`错误的时间区间 [${ startDate } - ${ endDate }]`)
        }

        const res: Array<WeekInfo> = []
        let temp = start
        let index = 1
        while (temp.unix() < end.unix()) {
            const startDayOfWeek = temp.day()

            // 第一天是周末
            if (startDayOfWeek === 0) {
                res.push(new WeekInfo(temp, temp))
                temp = temp.add(1, 'day')
                index++
                continue
            }
            const tempEnd = temp.add(7 - temp.day(), 'day')

            // 可能越界的最后一天
            if (tempEnd.unix() > end.unix()) {
                res.push(new WeekInfo(temp, end))
                break
            }

            // 中间的完整周
            res.push(new WeekInfo(temp, tempEnd))
            temp = tempEnd.add(1, 'day')
            index++
        }

        return res
    }

}
